package uk.ac.imperial.gpaexpress.server;


import java.io.IOException;
import java.io.StringWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenStream;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import uk.ac.imperial.doc.gpa.syntax.GPACompiler;
import uk.ac.imperial.doc.gpa.syntax.GPACompiler_PCTMCCompilerPrototype.system_return;
import uk.ac.imperial.doc.gpa.syntax.GPALexer;
import uk.ac.imperial.doc.gpa.syntax.GPAParser;
import uk.ac.imperial.doc.jexpressions.constants.Constants;
import uk.ac.imperial.doc.jexpressions.expressions.AbstractExpression;
import uk.ac.imperial.doc.jexpressions.variables.ExpressionVariable;
import uk.ac.imperial.doc.pctmc.analysis.AbstractPCTMCAnalysis;
import uk.ac.imperial.doc.pctmc.analysis.PCTMCAnalysisPostprocessor;
import uk.ac.imperial.doc.pctmc.analysis.plotexpressions.PlotDescription;
import uk.ac.imperial.doc.pctmc.experiments.iterate.PCTMCIterate;
import uk.ac.imperial.doc.pctmc.experiments.iterate.PlotAtDescription;
import uk.ac.imperial.doc.pctmc.experiments.iterate.RangeSpecification;
import uk.ac.imperial.doc.pctmc.postprocessors.numerical.NumericalPostprocessor;
import uk.ac.imperial.doc.pctmc.representation.EvolutionEvent;
import uk.ac.imperial.doc.pctmc.representation.PCTMC;

import com.google.common.collect.Multimap;

@SuppressWarnings("serial")
public class Upload extends HttpServlet {
	String db_url = "jdbc:postgresql:richard";
	String db_user = "richard";
	String db_password = "rhymenoceros";
	
	Constants constants;											// aspects of gpepa model
	Map<ExpressionVariable,AbstractExpression> unfoldedVariables;
	PCTMC pctmc;
	Multimap<AbstractPCTMCAnalysis,PlotDescription> plots;
	List<PCTMCIterate> experiments;

	FileItem uploadItem;
	Connection conn;

	String model_title;		// models table
	String user;
	String source;
	String model_xml;	

	// params table
	int model_id;

	String type;			// analyses table
	int params_id;
	double stepSize;
	double stopTime;

	String iterateConstant;	// iterate table
	double fromVal;
	double toVal;
	int iterateSteps;

	int analysis_id;		// plots table
	int	plot_id;

	public void doGet(HttpServletRequest req, HttpServletResponse resp) {
		resp.setContentType("text/html");
		try{
			// Get session
			HttpSession session = req.getSession(true);
			user = (String) session.getAttribute("email");
			model_title = req.getParameter("modelTitleElement");
			source = req.getParameter("modelTextElement").replace("\r", "");
			resp.getWriter().write(parseSource());
			
		}catch(Exception e){
			e.printStackTrace();
		}
	}
	
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
	throws ServletException, IOException {
		resp.setContentType("text/html");
		try {
			// Get session
			HttpSession session = req.getSession(true);
			user = (String) session.getAttribute("email");

			// Get uploaded file from request
			uploadItem = getFileItem(req);

			// If no file present then return
			if(uploadItem == null)
				return;

			// Extract source string and filename from file
			source = uploadItem.getString();
			model_title = uploadItem.getName();

			resp.getWriter().write(parseSource());

		} catch (Exception e) {
			System.err.println("Exception: " + e + "\n" + e.getMessage() );
		}

	}

	/* Function parses model source to get components */
	private String parseSource() throws SQLException, RecognitionException, ClassNotFoundException, ParserConfigurationException, TransformerException{
		// Parse source to get components of model
		CharStream charStream = new ANTLRStringStream(source);
		GPALexer lexer = new GPALexer(charStream);
		TokenStream tokenStream = new CommonTokenStream(lexer);
		GPAParser parser = new GPAParser(tokenStream);
		CommonTreeNodeStream nodeStream = new CommonTreeNodeStream(parser.system().getTree());
		GPACompiler compiler = new GPACompiler(nodeStream);
		system_return system = compiler.system();
		constants = system.constants;
		unfoldedVariables = system.unfoldedVariables;
		pctmc = system.pctmc;
		plots = system.plots;
		experiments = system.experiments;			

		// Load the relevant RDBMS JDBC driver and initialise connection
		Class.forName( "org.postgresql.Driver" );
		conn = DriverManager.getConnection(
				db_url,db_user,db_password);

		// Save model to db relations
		saveModel();
		saveParams();
		saveAnalyses();
		saveExperiments();

		// Build model xml, return in response, and save in model table
		buildModelXml();
		saveModelXml();
		
		// Close db connection
		conn.close();
		
		return model_xml;	
	}
	
	/* Function saves model info to db */
	private void saveModel() throws SQLException, ParserConfigurationException{
		PreparedStatement update_models = conn.prepareStatement("INSERT INTO models(title,owner,source) VALUES (?, ?, ?)", 
				Statement.RETURN_GENERATED_KEYS);
		update_models.setString(1, model_title);			//title
		update_models.setString(2, user);	        		//owner
		update_models.setString(3, source);					//model_source
		update_models.executeUpdate();
		ResultSet modelKeys = update_models.getGeneratedKeys();
		// Get new model id from models table
		while(modelKeys.next()){
			model_id = modelKeys.getBigDecimal(1).intValueExact();
		}
		update_models.close();
	}

	/* Function saves model info to db */
	private void saveParams() throws SQLException{
		// Build change_constants string
		String change_constants = "";
		for(Entry<String, Double> e : constants.getConstantsMap().entrySet()){
			change_constants += e.getKey() + "," + String.valueOf(e.getValue()) + ";";
		}
		PreparedStatement update_params = conn.prepareStatement("INSERT INTO params(title,model_id,change_constants) VALUES (?,?,?)",
				Statement.RETURN_GENERATED_KEYS);
		update_params.setString(1, "default_params");
		update_params.setInt(2, model_id);
		update_params.setString(3, change_constants);
		update_params.executeUpdate();
		ResultSet paramKeys = update_params.getGeneratedKeys();
		while(paramKeys.next()){
			params_id = paramKeys.getBigDecimal(1).intValueExact();
		}
	}

	/* Function saves analyses info to db */
	private void saveAnalyses() throws SQLException{
		for (Entry<AbstractPCTMCAnalysis, Collection<PlotDescription>> a : plots.asMap().entrySet()){
			type = a.getKey().toString();
			PCTMCAnalysisPostprocessor pp = a.getKey().getPostprocessors().iterator().next();
			NumericalPostprocessor postprocessor = (NumericalPostprocessor) pp;
			stepSize = postprocessor.getStepSize();
			stopTime = postprocessor.getStopTime();
			PreparedStatement update_analyses = conn.prepareStatement(
					"INSERT INTO analyses(type,model_id,stepSize,stopTime) VALUES (?, ?, ?, ?)",
					Statement.RETURN_GENERATED_KEYS);
			update_analyses.setString(1,type);
			update_analyses.setInt(2,model_id);
			update_analyses.setDouble(3,stepSize);
			update_analyses.setDouble(4,stopTime);
			update_analyses.executeUpdate();
			ResultSet analysisKeys = update_analyses.getGeneratedKeys();
			// Get new analysis id
			if(analysisKeys.next()){
				analysis_id = analysisKeys.getBigDecimal(1).intValueExact();
			}
			update_analyses.close();
		}
	}

	/* Function saves experiments info to db */
	private void saveExperiments() throws SQLException{
		Iterator<PCTMCIterate> exp_iterator = experiments.iterator();
		PreparedStatement updateAnl = conn.prepareStatement(
				"INSERT INTO analyses(type,model_id,stepSize,stopTime) " +
				"VALUES(?, ?, ?, ?)",Statement.RETURN_GENERATED_KEYS);
		PreparedStatement updateItr = conn.prepareStatement(
				"INSERT INTO iterates(analysis_id,constant,fromVal,toVal,steps) " +
		"VALUES(?,?,?,?,?)");
		// For each experiment
		while(exp_iterator.hasNext()){
			PCTMCIterate exp = exp_iterator.next();
			NumericalPostprocessor npp = (NumericalPostprocessor) exp.getAnalysis().getPostprocessors().iterator().next();
			type = exp.toString();
			stepSize = npp.getStepSize();
			stopTime = npp.getStopTime();
			updateAnl.setString(1,type);
			updateAnl.setInt(2,model_id);
			updateAnl.setDouble(3,stepSize);
			updateAnl.setDouble(4,stopTime);
			updateAnl.executeUpdate();
			ResultSet analysisKeys = updateAnl.getGeneratedKeys();
			if(analysisKeys.next()){
				analysis_id = analysisKeys.getBigDecimal(1).intValueExact();
			}
			List<RangeSpecification> ranges = exp.getRanges();
			for(RangeSpecification r : ranges){
				iterateConstant = r.getConstant();
				fromVal = r.getFrom();
				toVal = r.getTo();
				iterateSteps = r.getSteps();

				updateItr.setInt(1,analysis_id);
				updateItr.setString(2,iterateConstant);
				updateItr.setDouble(3,fromVal);
				updateItr.setDouble(4,toVal);
				updateItr.setInt(5,iterateSteps);
				updateItr.execute();
			}
		}
		updateAnl.close();
		updateItr.close();
	}
	/* Function takes pctmc as input and builds xml structure to represent states and transitions */
	private void buildModelXml() throws ParserConfigurationException, TransformerException{
		DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
		DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
		Document document = docBuilder.newDocument();
		Element rootElement = document.createElement("model");
		rootElement.setAttribute("id", String.valueOf(model_id));
		rootElement.setAttribute("title", model_title);
		document.appendChild(rootElement);

		// Add source element
		Element sourceEm = document.createElement("source");
		sourceEm.setTextContent(source);
		rootElement.appendChild(sourceEm);

		// Model constants
		for(Entry<String, Double> c : constants.getConstantsMap().entrySet()){
			Element em = document.createElement("constant");
			em.setAttribute("name", c.getKey());
			em.setAttribute("value", c.getValue().toString());
			rootElement.appendChild(em);
		}

		// Model unfolded variables
		Set<ExpressionVariable> keySet = unfoldedVariables.keySet();
		for(ExpressionVariable var : keySet){
			Element em = document.createElement("variable");
			em.setAttribute("var", var.toString());
			em.setAttribute("expression", unfoldedVariables.get(var).toString());
			rootElement.appendChild(em);
		}

		// Model states
		Collection<EvolutionEvent> evolutionEvents = pctmc.getEvolutionEvents();
		Iterator<EvolutionEvent> iterator = evolutionEvents.iterator();
		while(iterator.hasNext()){
			EvolutionEvent event = iterator.next();
			for(int i=0; i<event.getDecreasing().size(); i++){
				Element em = document.createElement("transition");
				String dec = event.getDecreasing().get(i).toString();
				String inc = event.getIncreasing().get(i).toString();
				String r = event.getRate().toString();
				em.setAttribute("group", dec.substring(0, dec.indexOf(':')));
				em.setAttribute("rate", r.substring(r.indexOf('*')+2, r.indexOf(')', r.indexOf('*'))));
				em.setAttribute("down", dec.substring(dec.indexOf(':')+1));
				em.setAttribute("up", inc.substring(inc.indexOf(':')+1));
				rootElement.appendChild(em);
			}
		}

		// Model analyses
		for (Entry<AbstractPCTMCAnalysis, Collection<PlotDescription>> a : plots.asMap().entrySet()){
			Element analysisEm = document.createElement("analysis");
			analysisEm.setAttribute("type", a.getKey().toString());
			PCTMCAnalysisPostprocessor pp = a.getKey().getPostprocessors().iterator().next();
			NumericalPostprocessor postprocessor = (NumericalPostprocessor) pp;
			double stepSize = postprocessor.getStepSize();
			double stopTime = postprocessor.getStopTime();
			analysisEm.setAttribute("step_size", String.valueOf(stepSize));
			analysisEm.setAttribute("stop_time", String.valueOf(stopTime));
			rootElement.appendChild(analysisEm);
			Iterator<PlotDescription> plotsIterator = a.getValue().iterator();
			while(plotsIterator.hasNext()){
				PlotDescription plot = plotsIterator.next();
				Element plotEm = document.createElement("plot");
				plotEm.setAttribute("expression", plot.toString());
				analysisEm.appendChild(plotEm);
			}
		}

		//model experiments
		Iterator<PCTMCIterate> experimentsIterator = experiments.iterator();
		while(experimentsIterator.hasNext()){
			// Get analysis
			PCTMCIterate experiment = experimentsIterator.next();
			AbstractPCTMCAnalysis expAnalysis = experiment.getAnalysis();
			PCTMCAnalysisPostprocessor pp = expAnalysis.getPostprocessors().iterator().next();
			NumericalPostprocessor postprocessor = (NumericalPostprocessor) pp;
			double stepSize = postprocessor.getStepSize();
			double stopTime = postprocessor.getStopTime();

			// Create element for experiment
			Element experimentEm = document.createElement("experiment");
			rootElement.appendChild(experimentEm);

			// Append analysis to experiment element
			Element expAnalysisEm = document.createElement("exp_analysis");
			expAnalysisEm.setAttribute("type", expAnalysis.toString());
			expAnalysisEm.setAttribute("step_size", String.valueOf(stepSize));
			expAnalysisEm.setAttribute("stop_time", String.valueOf(stopTime));
			experimentEm.appendChild(expAnalysisEm);

			// Append expressions to analysis element
			Iterator<PlotAtDescription> expPlotIterator = experiment.getPlots().iterator();
			while(expPlotIterator.hasNext()){
				Element expPlotEm = document.createElement("exp_plot");
				expPlotEm.setAttribute("expression", expPlotIterator.next().toString());
				expAnalysisEm.appendChild(expPlotEm);
			}

			Iterator<RangeSpecification> expItr = experiment.getRanges().iterator();
			while(expItr.hasNext()){
				RangeSpecification range = expItr.next();
				String constant = range.getConstant();
				double fromVal = range.getFrom();
				double toVal = range.getTo();
				int steps = range.getSteps();
				Element iterateEm = document.createElement("iterate");
				iterateEm.setAttribute("constant", constant);
				iterateEm.setAttribute("fromVal", String.valueOf(fromVal));
				iterateEm.setAttribute("toVal", String.valueOf(toVal));
				iterateEm.setAttribute("steps", String.valueOf(steps));
				experimentEm.appendChild(iterateEm);
			}

		}

		TransformerFactory transfac = TransformerFactory.newInstance();
		Transformer trans = transfac.newTransformer();
		trans.setOutputProperty(OutputKeys.INDENT, "yes");

		//create string from xml tree
		StringWriter sw = new StringWriter();
		StreamResult result = new StreamResult(sw);
		DOMSource domSource = new DOMSource(document);
		trans.transform(domSource, result);
		model_xml = sw.toString();
	}

	/* Method saves model_xml for model with model_id */
	private void saveModelXml() throws SQLException{
		PreparedStatement save_xml = conn.prepareStatement("UPDATE models SET model_xml = ? WHERE id = ?");
		save_xml.setString(1, model_xml);
		save_xml.setInt(2, model_id);
		save_xml.execute();
		save_xml.close();
	}

	/* Function parses request to extract item */
	@SuppressWarnings("rawtypes")
	private FileItem getFileItem(HttpServletRequest req) {
		FileItemFactory factory = new DiskFileItemFactory();
		ServletFileUpload upload = new ServletFileUpload(factory);

		try {
			List items = upload.parseRequest(req);
			Iterator it = items.iterator();

			while(it.hasNext()) {
				FileItem item = (FileItem) it.next();
				if(!item.isFormField() && "uploadFormElement".equals(item.getFieldName())) {
					return item;
				}
			}
		}
		catch(FileUploadException e){
			return null;
		}

		return null;
	}
}

